Rescue from AR:SubclassNotFound and allow to delete agents

Especially when using Agent gems it happens that an Agent was deleted from the gem or the user removed a gem from the
configuration. Instead of failing with an Internal Server Error the user now is offered to delete all undefined agents.

`type.constantize` is needed to invoke the Rails auto_loader in development since not all Agent classes are loaded when
the application boots.

Dominik Sander 7 years ago
parent
commit
42b132017f

+ 6 - 0
app/controllers/agents_controller.rb

@@ -208,6 +208,12 @@ class AgentsController < ApplicationController
208 208
     render json: @agent.complete_option(params[:attribute])
209 209
   end
210 210
 
211
+  def destroy_undefined
212
+    current_user.undefined_agents.destroy_all
213
+
214
+    redirect_back "All undefined Agents have been deleted."
215
+  end
216
+
211 217
   protected
212 218
 
213 219
   # Sanitize params[:return] to prevent open redirect attacks, a common security issue.

+ 6 - 0
app/controllers/application_controller.rb

@@ -6,6 +6,12 @@ class ApplicationController < ActionController::Base
6 6
 
7 7
   helper :all
8 8
 
9
+  rescue_from 'ActiveRecord::SubclassNotFound' do
10
+    @undefined_agent_types = current_user.undefined_agent_types
11
+
12
+    render template: 'application/undefined_agents'
13
+  end
14
+
9 15
   def redirect_back(fallback_path, *args)
10 16
     redirect_to :back, *args
11 17
   rescue ActionController::RedirectBackError

+ 4 - 0
app/helpers/application_helper.rb

@@ -113,4 +113,8 @@ module ApplicationHelper
113 113
 
114 114
     @highlighted_ranges.any? { |range| range.cover?(id) }
115 115
   end
116
+
117
+  def agent_type_to_human(type)
118
+    type.gsub(/^.*::/, '').underscore.humanize.titleize
119
+  end
116 120
 end

+ 15 - 0
app/models/user.rb

@@ -75,4 +75,19 @@ class User < ActiveRecord::Base
75 75
   def requires_no_invitation_code?
76 76
     !!@requires_no_invitation_code
77 77
   end
78
+
79
+  def undefined_agent_types
80
+    agents.reorder('').group(:type).pluck(:type).select do |type|
81
+      begin
82
+        type.constantize
83
+        false
84
+      rescue NameError
85
+        true
86
+      end
87
+    end
88
+  end
89
+
90
+  def undefined_agents
91
+    agents.where(type: undefined_agent_types).select('id, schedule, type as undefined')
92
+  end
78 93
 end

+ 1 - 1
app/views/agents/_form.html.erb

@@ -25,7 +25,7 @@
25 25
           <% if @agent.new_record? %>
26 26
             <div class="form-group type-select">
27 27
               <%= f.label :type %>
28
-              <%= f.select :type, options_for_select([['Select an Agent Type', 'Agent', {title: ''}]] + Agent.types.map {|type| [type.name.gsub(/^.*::/, '').underscore.humanize.titleize, type, {title: h(Agent.build_for_type(type.name,current_user,{}).html_description.lines.first.strip)}] }, @agent.type), {}, :class => 'form-control' %>
28
+              <%= f.select :type, options_for_select([['Select an Agent Type', 'Agent', {title: ''}]] + Agent.types.map {|type| [agent_type_to_human(type.name), type, {title: h(Agent.build_for_type(type.name,current_user,{}).html_description.lines.first.strip)}] }, @agent.type), {}, :class => 'form-control' %>
29 29
             </div>
30 30
           <% end %>
31 31
         </div>

+ 44 - 0
app/views/application/undefined_agents.html.erb

@@ -0,0 +1,44 @@
1
+<div class="container">
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h3>
6
+          <div class="alert alert-danger" role="alert">
7
+            Error: Agent(s) are 'missing in action'
8
+          </div>
9
+        </h3>
10
+      </div>
11
+      <blockquote>
12
+        <p>
13
+          You have one or more Agents registered in the database for which no corresponding definition is available in the source code:
14
+        </p>
15
+        <ul>
16
+          <% @undefined_agent_types.each do |type| %>
17
+            <li><%= agent_type_to_human(type) %></li>
18
+          <% end %>
19
+        </ul>
20
+        <br/>
21
+        <p>
22
+          The issue most probably occurred because of one or more of the following reasons:
23
+        </p>
24
+        <ul>
25
+          <li>If the respective Agent is distributed as a Ruby gem, it might have been removed from the <code>ADDITIONAL_GEMS</code> environment setting.</li>
26
+          <li>If the respective Agent is distributed as part of the Huginn application codebase, it might have been removed from that either on purpose (because the Agent has been deprecated or been moved to an Agent gem) or accidentally. Please check if the Agent(s) in question are available in your Huginn codebase under the path <code>app/models/agents/</code>.</li>
27
+        </ul>
28
+        <br/>
29
+        <p>
30
+          You can fix the issue by adding the Agent(s) back to the application codebase by
31
+        </p>
32
+        <ul>
33
+          <li>adding the respective Agent(s) to the the <code>ADDITIONAL_GEMS</code> environment setting. Please see <a href="https://github.com/cantino/huginn_agent" target="_blank">https://github.com/cantino/huginn_agent</a> for documentation on how to properly set it.</li>
34
+          <li>adding the respective Agent(s) code to the Huginn application codebase (in case it was deleted accidentally).</li>
35
+          <li>deleting the respective Agent(s) from the database using the button below.</li>
36
+        </ul>
37
+        <br/>
38
+        <div class="btn-group">
39
+          <%= link_to icon_tag('glyphicon-trash') + ' Delete Missing Agents', undefined_agents_path, class: "btn btn-danger", method: :DELETE, data: { confirm: 'Are you sure all missing Agents should be deleted from the database?'} %>
40
+        </div>
41
+      </blockquote>
42
+    </div>
43
+  </div>
44
+</div>

+ 1 - 0
config/routes.rb

@@ -15,6 +15,7 @@ Huginn::Application.routes.draw do
15 15
       get :event_descriptions
16 16
       post :validate
17 17
       post :complete
18
+      delete :undefined, action: :destroy_undefined
18 19
     end
19 20
 
20 21
     resources :logs, :only => [:index] do

+ 14 - 0
spec/controllers/agents_controller_spec.rb

@@ -434,4 +434,18 @@ describe AgentsController do
434 434
       expect(agent.reload.memory).to eq({ "test" => 42})
435 435
     end
436 436
   end
437
+
438
+  describe 'DELETE undefined' do
439
+    it 'removes an undefined agent from the database' do
440
+      sign_in users(:bob)
441
+      agent = agents(:bob_website_agent)
442
+      agent.update_attribute(:type, 'Agents::UndefinedAgent')
443
+      agent2 = agents(:jane_website_agent)
444
+      agent2.update_attribute(:type, 'Agents::UndefinedAgent')
445
+
446
+      expect {
447
+        delete :destroy_undefined
448
+      }.to change { Agent.count }.by(-1)
449
+    end
450
+  end
437 451
 end

+ 21 - 0
spec/features/undefined_agents_spec.rb

@@ -0,0 +1,21 @@
1
+require 'capybara_helper'
2
+
3
+describe "handling undefined agents" do
4
+  before do
5
+    login_as(users(:bob))
6
+    agent = agents(:bob_website_agent)
7
+    agent.update_attribute(:type, 'Agents::UndefinedAgent')
8
+  end
9
+
10
+  it 'renders the error page' do
11
+    visit agents_path
12
+    expect(page).to have_text("Error: Agent(s) are 'missing in action'")
13
+    expect(page).to have_text('Undefined Agent')
14
+  end
15
+
16
+  it 'deletes all undefined agents' do
17
+    visit agents_path
18
+    click_on('Delete Missing Agents')
19
+    expect(page).to have_text('Your Agents')
20
+  end
21
+end

+ 27 - 0
spec/models/users_spec.rb

@@ -1,6 +1,8 @@
1 1
 require 'rails_helper'
2 2
 
3 3
 describe User do
4
+  let(:bob) { users(:bob) }
5
+
4 6
   describe "validations" do
5 7
     describe "invitation_code" do
6 8
       context "when configured to use invitation codes" do
@@ -64,4 +66,29 @@ describe User do
64 66
       expect(users(:bob).deactivated_at).to be_nil
65 67
     end
66 68
   end
69
+
70
+  context '#undefined_agent_types' do
71
+    it 'returns an empty array when no agents are undefined' do
72
+      expect(bob.undefined_agent_types).to be_empty
73
+    end
74
+
75
+    it 'returns the undefined agent types' do
76
+      agent = agents(:bob_website_agent)
77
+      agent.update_attribute(:type, 'Agents::UndefinedAgent')
78
+      expect(bob.undefined_agent_types).to match_array(['Agents::UndefinedAgent'])
79
+    end
80
+  end
81
+
82
+  context '#undefined_agents' do
83
+    it 'returns an empty array when no agents are undefined' do
84
+      expect(bob.undefined_agents).to be_empty
85
+    end
86
+
87
+    it 'returns the undefined agent types' do
88
+      agent = agents(:bob_website_agent)
89
+      agent.update_attribute(:type, 'Agents::UndefinedAgent')
90
+      expect(bob.undefined_agents).not_to be_empty
91
+      expect(bob.undefined_agents.first).to be_a(Agent)
92
+    end
93
+  end
67 94
 end